using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace FederatedScaleOutDatabases.Utilities.LINQ
{
    public class MemberParams
    {
        public Object ValueToCompare { get; set; }
        public ExpressionType Operator { get; set; }
    }

    public sealed class ExpressionEquivalenceComparer<TA> : ExpressionVisitor
    {
        private class ExpressionContext
        {
            public Expression LeftExpression { get; set; }
            public Tuple<MemberInfo, object> Mapping { get; set; }
            public bool? MatchedWithMapping { get; set; }
            public ExpressionType LeftAndRightJoinedBy { get; set; }
            public Expression RightExpression { get; set; }

        }

        private readonly Dictionary<Expression, ExpressionContext> _expressionContexts = new Dictionary<Expression, ExpressionContext>();

        private readonly Dictionary<MemberInfo, MemberParams> _mappings;


        public bool Result { get; set; }

        protected override Expression VisitBinary(BinaryExpression node)
        {
            if (node.NodeType == ExpressionType.Or || node.NodeType == ExpressionType.OrElse || node.NodeType == ExpressionType.And || node.NodeType == ExpressionType.AndAlso)
            {
                var leftNodecontext = new ExpressionContext()
                {
                    LeftExpression = node.Left,
                    RightExpression = node.Right,
                    LeftAndRightJoinedBy = node.NodeType,
                    Mapping = null
                };

                _expressionContexts.Add(node.Left, leftNodecontext);

                this.Visit(node.Left);

                var rightNodecontext = new ExpressionContext()
                {
                    LeftExpression = node.Left,
                    RightExpression = node.Right,
                    LeftAndRightJoinedBy = node.NodeType,
                    Mapping = null
                };

                _expressionContexts.Add(node.Right, rightNodecontext);

                this.Visit(node.Right);

                return node;
            }

            if (node.NodeType == ExpressionType.Equal
                || node.NodeType == ExpressionType.NotEqual
                || node.NodeType == ExpressionType.LessThan
                || node.NodeType == ExpressionType.GreaterThan
                || node.NodeType == ExpressionType.LessThanOrEqual
                || node.NodeType == ExpressionType.GreaterThanOrEqual)
            {
                var leftExpression = this.Visit(node.Left);
                var rightExpression = this.Visit(node.Right);

                if (leftExpression is MemberExpression)
                {
                    var memberInfo = (leftExpression as MemberExpression).Member;
                    MemberParams memberParams;
                    if (this._mappings.TryGetValue(memberInfo, out memberParams))
                    {
                        var tupleKey = new Tuple<MemberInfo, Object>(memberInfo, memberParams);
                        var context = new ExpressionContext()
                                          {
                                              LeftExpression = node.Left,
                                              RightExpression = node.Right,
                                              LeftAndRightJoinedBy = node.NodeType,
                                              Mapping = tupleKey,
                                              MatchedWithMapping = true
                                          };
                        context.MatchedWithMapping = EvaluateMatchingWithMapping(node, rightExpression, memberParams);

                        if (_expressionContexts.ContainsKey(node))
                        {
                            _expressionContexts[node] = context;
                        }
                        else
                        {
                            _expressionContexts.Add(node, context);
                        }

                    }
                    else //members not found in mapping
                    {

                        var tupleKey = new Tuple<MemberInfo, Object>(memberInfo, rightExpression);
                        var context = new ExpressionContext()
                                          {
                                              LeftExpression = node.Left,
                                              RightExpression = node.Right,
                                              LeftAndRightJoinedBy = node.NodeType,
                                              Mapping = tupleKey,
                                              MatchedWithMapping = false
                                          };

                        if (_expressionContexts.ContainsKey(node))
                        {
                            _expressionContexts[node] = context;
                        }
                        else
                        {
                            _expressionContexts.Add(node, context);
                        }

                    }
                }

                return node;
            }

            return base.VisitBinary(node);
        }

        private static bool EvaluateMatchingWithMapping(BinaryExpression node, Expression rightExpression, MemberParams memberParams)
        {
            dynamic valueFromMapping = null;
            dynamic valueFromExpression = null;

            if (rightExpression is ConstantExpression)
            {
                valueFromMapping = memberParams.ValueToCompare;
                valueFromExpression = (rightExpression as ConstantExpression).Value;
            }

            else if (rightExpression is NewExpression)
            {
                var newExpression = (rightExpression as NewExpression);
                var typeName = (rightExpression as NewExpression).Type.Name;

                if (typeName == typeof(DateTime).Name) //currently, we will support datetime with yyyy,mm,dd constructor only
                {
                    if (newExpression.Constructor.GetParameters().Count() == 3)
                    {
                        var constantExpressionFirstArg = newExpression.Arguments[0] as ConstantExpression;
                        if (constantExpressionFirstArg != null)
                        {
                            var constantExpressionSecondArg = newExpression.Arguments[1] as ConstantExpression;
                            if (constantExpressionSecondArg != null)
                            {
                                var constantExpressionThirdArg = newExpression.Arguments[2] as ConstantExpression;
                                if (constantExpressionThirdArg != null)
                                {
                                    var expressionDate = new DateTime((Int32)constantExpressionFirstArg.Value,
                                                                      (Int32)constantExpressionSecondArg.Value,
                                                                      (Int32)constantExpressionThirdArg.Value);

                                    var valueToCompareDate = (DateTime)memberParams.ValueToCompare;
                                    valueFromMapping = valueToCompareDate;
                                    valueFromExpression = expressionDate;
                                }
                            }
                        }
                    }
                }
            }


            Func<ExpressionType, dynamic, dynamic, bool> compararer = (nodeType, mappingValue, expressionValue) => mappingValue == expressionValue;

            return (memberParams.Operator == node.NodeType)
                           && compararer(node.NodeType, valueFromMapping, valueFromExpression);
        }

        protected override Expression VisitMember(MemberExpression node)
        {
            if (node.Expression == null || node.Expression.Type != typeof(TA))
            {
                return base.VisitMember(node);
            }
            var expression = this.Visit(node.Expression);


            return Expression.Property(expression, node.Member.Name);
        }

        protected override Expression VisitLambda<T>(Expression<T> node)
        {
            var lambdaExpression = Expression.Lambda(this.Visit(node.Body), node.Parameters.Select(this.Visit).Cast<ParameterExpression>());

            var binaryNode = (node.Body as BinaryExpression);


            var mainContextValue = _expressionContexts.FirstOrDefault(c => binaryNode != null && (c.Value.LeftExpression == binaryNode.Left && c.Value.RightExpression == binaryNode.Right)).Value;


            //if left and right expressions are simple expressions
            var leftNodeContextValue = _expressionContexts.SingleOrDefault(c => binaryNode != null && c.Key == binaryNode.Left).Value;
            var rightNodeContextValue = _expressionContexts.SingleOrDefault(c => binaryNode != null && c.Key == binaryNode.Right).Value;
            if ((leftNodeContextValue != null
                && leftNodeContextValue.LeftExpression.NodeType == ExpressionType.MemberAccess
                && (leftNodeContextValue.RightExpression.NodeType == ExpressionType.Constant
                || leftNodeContextValue.RightExpression.NodeType == ExpressionType.New
                ))
                && (rightNodeContextValue != null
                && rightNodeContextValue.LeftExpression.NodeType == ExpressionType.MemberAccess
                && (rightNodeContextValue.RightExpression.NodeType == ExpressionType.Constant
                || rightNodeContextValue.RightExpression.NodeType == ExpressionType.New
                )))
            {
                if (binaryNode != null && binaryNode.NodeType == ExpressionType.AndAlso)
                {
                    Result = leftNodeContextValue.MatchedWithMapping.GetValueOrDefault(false)
                             || //For And, doing || operation since, if one of the expressions of AND is matched, further restriction shouldn't affect it
                             rightNodeContextValue.MatchedWithMapping.GetValueOrDefault(false);
                    return lambdaExpression;
                }
                else if (binaryNode != null && binaryNode.NodeType == ExpressionType.OrElse)
                {
                    Result = leftNodeContextValue.MatchedWithMapping.GetValueOrDefault(false)
                             && //For OR, doing && operation since, even if one of the expressions of OR is matched, further restriction can affect it
                             rightNodeContextValue.MatchedWithMapping.GetValueOrDefault(false);
                    return lambdaExpression;
                }
            }
            else if (mainContextValue.LeftAndRightJoinedBy == ExpressionType.Equal //Lambda expression that is not joined by && or || e.g. emp => emp.DeptId > 100
                 || mainContextValue.LeftAndRightJoinedBy == ExpressionType.NotEqual
                || mainContextValue.LeftAndRightJoinedBy == ExpressionType.LessThan
                || mainContextValue.LeftAndRightJoinedBy == ExpressionType.GreaterThan
                || mainContextValue.LeftAndRightJoinedBy == ExpressionType.LessThanOrEqual
                || mainContextValue.LeftAndRightJoinedBy == ExpressionType.GreaterThanOrEqual)
            {
                Result = EvaluateContext(_expressionContexts.FirstOrDefault(
                                                                            c => binaryNode != null
                                                                         && (c.Value.LeftExpression == binaryNode.Left
                                                                             && c.Value.RightExpression == binaryNode.Right)).Key);
            }
            else
            {

                var leftExpressionResult = binaryNode != null && EvaluateContext(binaryNode.Left);
                var rightExpressionResult = binaryNode != null && EvaluateContext(binaryNode.Right);

                if (mainContextValue.LeftAndRightJoinedBy == ExpressionType.AndAlso)
                    Result = leftExpressionResult || rightExpressionResult;
                else
                    Result = leftExpressionResult && rightExpressionResult;
            }


            return lambdaExpression;
        }

        private bool EvaluateContext(Expression expression)
        {
            var matchingFound = _expressionContexts.Count(c => (c.Key == expression && c.Value.MatchedWithMapping == true));
            if (matchingFound > 0)
            {
                return true;
            }
            if (expression.NodeType == ExpressionType.Equal)//If there is no mapping for Equality
            {
                return false;
            }
            if (expression is BinaryExpression && (expression.NodeType == ExpressionType.AndAlso || expression.NodeType == ExpressionType.OrElse))
            {
                var leftOutput = EvaluateContext((expression as BinaryExpression).Left);
                var rightOutput = EvaluateContext((expression as BinaryExpression).Right);
                return leftOutput && rightOutput;
            }
            if (expression.NodeType == ExpressionType.Equal)
                return EvaluateContext(expression);
            return false;
        }

        public ExpressionEquivalenceComparer(Dictionary<MemberInfo, MemberParams> mappings)
        {
            this._mappings = mappings;
        }
    }
}
